package com.app.core.mvc.repeat;

import cn.hutool.core.map.MapBuilder;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.app.core.mvc.result.Code;
import com.app.core.mvc.servlet.CustomHttpServletRequest;
import com.app.core.security.ResourceOwnerContext;
import com.app.kit.IPUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 防重复提交校验拦截器
 * 背景：防止新增、编辑在页面的重复提交。
 * 基本思路：判断redis保存key = md5(路径+请求参数request.getParameterMap())是否存在
 * 1、配置文件新增开关控制拦截器的开启|关闭
 * 2、新增方法级别的注解，只有加了注解的controller方法才校验重复提交
 * 3、新增拦截器（已有RepeatCommitInterceptor.java）
 * RepeatCommit:e10adc3949ba59abbe56e057f20f883e=182872727
 *
 * @author qiangt
 * @since 2022-11-25
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RepeatCommitInterceptor implements HandlerInterceptor {

    /** 本次请求redis中的value，固定值即可，主要是判断有没有 */
    private static final String REDIS_VALUE = "RepeatCommitCheck";
    /** 本次请求redis中的key的前缀 */
    private static final String REDIS_KEY_PREFIX = "RepeatCommit";
    /** 异常信息格式化 */
    private static final String STRING_FORMAT_EXCEPTION = "RepeatCommitInterceptor -> %s";
    /** 默认两次请求之间的最小时间间隔（单位：毫秒）：100 */
    private static final long DEFAULT_REPEAT_COMMIT_INTERVAL = 100;
    /** 重复提交缓存操作类 */
    private final RepeatCommitCache repeatCommitCache;
    /** 当前登录人 */
    private final ResourceOwnerContext resourceOwnerContext;
    /** 重复提交yml配置类 */
    private final RepeatCommitProperties repeatCommitProperties;
    /** 是否启用防重复提交校验 */
    private Boolean repeatCommitEnabled;
    /**
     * 两次请求之间的最小时间间隔（单位：毫秒）
     * 可能来自@RepeatCommitCheck，yml或者默认值
     */
    private Long repeatCommitInterval;
    private HttpServletRequest request;
    /** 当前接口方法名 */
    private String methodName;

    /**
     * 拦截器预处理
     * 校验md5(路径+请求参数request.getParameterMap())是否存在
     *
     * @param request
     * @param response
     * @param handler
     * @return
     */
    @Override
    public boolean preHandle(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response, @NotNull final Object handler) {
        // 如果是静态资源，直接放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // 缓存当前请求
        this.request = request;
        // 不做防重复提交校验直接放行
        if (!this.isRepeatCommitEnabled()) {
            return true;
        }
        final RepeatCommitCheck repeatCommitCheck = this.findRepeatCommitCheckAnnotation((HandlerMethod) handler);
        // 未加防重复提交校验注解的直接放行
        if (Objects.isNull(repeatCommitCheck)) {
            return true;
        }
        methodName = this.getMethodName((HandlerMethod) handler);
        CacheKey cacheKey = this.createCacheKey(this.createCacheSuffix());
        // key存在则表示在规定时间间隔内提交了多次
        if (Objects.nonNull(this.getCache(cacheKey))) {
            throw Code.A00017.toCodeException();
        }
        log.info("防重复提交校验通过->{}", request.getRequestURI());
        // key不存在则将当前key放入缓存中，拦截下一次的重复提交
        this.insertCache(cacheKey, repeatCommitCheck.interval());
        return true;
    }

    @Override
    public void postHandle(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response, @NotNull final Object handler, final ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response, @NotNull final Object handler, final Exception e) {

    }

    /** 防重复提交校验是否开启 */
    private boolean isRepeatCommitEnabled() {
        if (Objects.isNull(this.repeatCommitEnabled)) {
            this.repeatCommitEnabled = this.repeatCommitProperties.isEnabled();
        }
        return this.repeatCommitEnabled;
    }

    /** 类或者方法上是否加了RepeatCommitCheck注解 */
    private RepeatCommitCheck findRepeatCommitCheckAnnotation(final HandlerMethod handlerMethod) {
        RepeatCommitCheck repeatCommitCheck = handlerMethod.getBeanType().getAnnotation(RepeatCommitCheck.class);
        if (Objects.isNull(repeatCommitCheck)) {
            repeatCommitCheck = handlerMethod.getMethodAnnotation(RepeatCommitCheck.class);
        }
        return repeatCommitCheck;
    }

    /** 获取当前接口方法名 */
    private String getMethodName(final HandlerMethod handlerMethod) {
        String className = handlerMethod.getBeanType().getSimpleName();
        String methodName = handlerMethod.getMethod().getName();
        return className.concat(methodName);
    }

    /** 生成缓存的key */
    private CacheKey createCacheKey(final String suffix) {
        if (StringUtils.isEmpty(suffix)) {
            throw new IllegalArgumentException(String.format(STRING_FORMAT_EXCEPTION, "防重复提交前缀不能为空"));
        }
        return new CacheKey(String.format("%s:%s", REDIS_KEY_PREFIX, suffix));
    }

    /** 计算方法：md5(路径+请求参数request.getParameterMap()) */
    private String createCacheSuffix() {
        // 获取所有应该参与签名的参数
        final Map<String, String> parameters
                = this.mergeParameters(this.getAllRequestParameters(), this.getExtraParameter());
        // 组装每一个参数
        final List<String> formatParameters = this.format(parameters);
        // 根据组装完成的参数生成签名
        return this.generateSign(formatParameters);
    }

    /** 获取缓存 */
    private String getCache(final CacheKey cacheKey) {
        if (Objects.isNull(cacheKey)) {
            throw new IllegalArgumentException(String.format(STRING_FORMAT_EXCEPTION, "查询key不能为空"));
        }
        final String value = this.repeatCommitCache.get(cacheKey.getValue());
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        return value;
    }

    /** 插入缓存 */
    private void insertCache(final CacheKey cacheKey, final long interval) {
        if (Objects.isNull(cacheKey)) {
            throw new IllegalArgumentException(String.format(STRING_FORMAT_EXCEPTION, "插入key不能为空"));
        }
        this.repeatCommitCache.set(cacheKey.getValue(), REDIS_VALUE, this.getRepeatCommitInterval(interval), TimeUnit.MILLISECONDS);
    }

    /** 合并参数 */
    private Map<String, String> mergeParameters(Map<String, String> left, Map<String, String> right) {
        if (MapUtils.isEmpty(left)) {
            left = new HashMap<>();
        }
        if (MapUtils.isEmpty(right)) {
            right = new HashMap<>();
        }
        left.putAll(right);
        return left;
    }

    /** 获取所有的请求参数，包括URL参数和body中的参数 */
    private Map<String, String> getAllRequestParameters() {
        return this.mergeParameters(this.getUrlParameters(), this.getJSONParameters());
    }

    /** 获取额外参数 */
    private Map<String, String> getExtraParameter() {
        final HttpServletRequest request = this.getRequest();
        String identity = this.resourceOwnerContext.getPrincipal().getIdentity().toString();
        return MapBuilder.create(new HashMap<String, String>())
                .put("method", request.getMethod())
                // .put("path", request.getRequestURI()) // path换成类名+方法名，为了解决path传参问题
                .put("path", this.methodName)
                .put("ip", IPUtils.getIpAddr(request))
                .build();
    }

    /** 组装参数 */
    private List<String> format(final Map<String, String> parameters) {
        return parameters.entrySet()
                .stream()
                .filter(e -> Objects.nonNull(e.getValue()) && StringUtils.isNotEmpty(e.getValue()))
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.toList());
    }

    /** 生成签名 */
    private String generateSign(final List<String> list) {
        log.info("字符串列表为：{}", JSON.toJSONString(list));
        // 进行自然排序
        Collections.sort(list);
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < list.size(); i++) {
            final String s = list.get(i);
            if (i == 0) {
                sb.append(s);
            } else {
                sb.append("&").append(s);
            }
        }
        final String content = sb.toString();
        log.info("拼接后的字符串为：" + content);
        String sign = null;
        try {
            // 加密
            final byte[] encodeValue = this.doEncode(content.getBytes(StandardCharsets.UTF_8));
            // 转化为十六进制大写字母
            sign = this.byteToHexString(encodeValue);
        } catch (final Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        log.info("签名为：" + sign);
        return sign;
    }

    /** 获取防重复提交时间间隔 */
    private long getRepeatCommitInterval(final long interval) {
        // 先用全局配置
        if (Objects.isNull(this.repeatCommitInterval)) {
            this.repeatCommitInterval = this.repeatCommitProperties.getRepeatInterval();
        }
        // 如果配置局部时间间隔，则覆盖全局配置
        if (interval > 0) {
            this.repeatCommitInterval = interval;
        }
        // 如果2者都没有配置，则使用默认的时间间隔
        if (Objects.isNull(this.repeatCommitInterval)) {
            this.repeatCommitInterval = DEFAULT_REPEAT_COMMIT_INTERVAL;
        }
        return this.repeatCommitInterval;
    }

    /** 获取URL拼接参数 */
    private Map<String, String> getUrlParameters() {
        final HttpServletRequest request = this.getRequest();
        final Map<String, String[]> parameters = request.getParameterMap();
        final Map<String, String> result = new HashMap<>(parameters.size());
        if (MapUtils.isEmpty(parameters)) {
            return new HashMap<>(0);
        }
        for (final Map.Entry<String, String[]> entry : parameters.entrySet()) {
            result.put(entry.getKey(), this.formatUrlParameterStringArray(entry.getValue()));
        }
        return result;
    }

    /** 获取request body中的JSON参数 */
    private Map<String, String> getJSONParameters() {
        final HttpServletRequest request = this.getRequest();
        // 非JSON请求不做处理
        if (Objects.isNull(request.getContentType()) || !request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
            return new HashMap<>(0);
        }
        return this.doGetJSONParameters();
    }

    private HttpServletRequest getRequest() {
        if (Objects.isNull(this.request)) {
            if (Objects.nonNull(RequestContextHolder.getRequestAttributes())) {
                this.request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            }
        }
        return this.request;
    }

    /** 数字签名 */
    private byte[] doEncode(final byte[] value) {
        return SecureUtil.md5().digest(value);
    }

    /** 转化为十六进制大写字母 */
    private String byteToHexString(final byte[] digest) {
        final StringBuilder sb = new StringBuilder();
        for (final byte b : digest) {
            final String hexString = Integer.toHexString(b & 0xff);
            if (hexString.length() == 1) {
                sb.append("0").append(hexString);
            } else {
                sb.append(hexString);
            }
        }
        return sb.toString().toUpperCase();
    }

    /** 组装URL拼接参数 */
    private String formatUrlParameterStringArray(final String[] value) {
        if (ArrayUtils.isEmpty(value)) {
            return null;
        }
        // 按自然顺序排序
        return Stream.of(value).sorted().collect(Collectors.joining(","));
    }

    /** 获取request body中的JSON参数 */
    private Map<String, String> doGetJSONParameters() {
        final CustomHttpServletRequest request = (CustomHttpServletRequest) this.getRequest();
        final String body = request.getBodyString();
        if (StringUtils.isEmpty(body)) {
            return new HashMap<>(0);
        }
        return MapBuilder.create(new HashMap<String, String>(1))
                .put("body", body)
                .build();
    }

    /** 缓存key静态内部类 */
    private static class CacheKey {
        String value;

        public CacheKey(final String value) {
            this.value = value;
        }

        public String getValue() {
            return this.value;
        }

        public void setValue(final String value) {
            this.value = value;
        }
    }

}

